---
description: Create a data visualization theme for your project
---
import React, { useState, useRef } from 'react'
import BrowserOnly from '@docusaurus/BrowserOnly'
import CodeBlock from '@theme/CodeBlock'

import { CSSVariables } from '../components/css-variables'
import { data, generateScatterDataRecords, generateTimeSeries } from '../utils/data.ts'
import { DocWrapper } from '../wrappers'
import { XYWrapper, axis } from '../wrappers/xy-wrapper'
import { sankeyProps as sankeyProps } from '../networks-and-flows/Sankey.mdx'
import { donutProps as donutProps } from '../misc/Donut.mdx'

import './styles.css'

# Theming

export const xyProps = (n) => ({
  data: data.map(d => ({
    ...d,
    values: Array(10).fill(0).map(() => Math.random())
  })),
  x: d => d.x,
  y: Array(n).fill(0).map((_, i) => d => d.values[i]),
  excludeTabs: true,
})

export const borderLineProps = {
  ...xyProps(7),
  height: 150,
}

export const scatterProps = {
  name: 'Scatter',
  x: d => d.x,
  y: d => d.y,
  label: d => d.label,
  excludeTabs: true,
  components: [
    axis('x', { label: 'X Axis'}),
    axis('y', { label: 'Y Axis'}),
  ]
}

export function SizedScatter (props) {
  const data = [6,8,4,6,5].map((d,i) => ({
    x: i,
    y: d,
    color: `var(--vis-color${i})`,
    label: `Point ${String.fromCharCode(65 + i)}`
  }))
  return (
    <BrowserOnly>{() =>
     <XYWrapper
      {...scatterProps}
      data={data}
      className={props.containerClass ? require('@unovis/ts')[props.containerClass] : ''}
    />
    }</BrowserOnly>
  )
}

## CSS Variables
### Overview
In addition to configuration properties, our components also rely on
[CSS variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties)
to supply the values of various SVG attributes such as `fill`, `stroke`, `opacity`, etc. You can override
these variables to further customize your _Unovis_ components.

Every variable has the following naming convention: `--vis` + _label_ + _attribute_. For example, the variable named
`--vis-area-cursor` would apply to the [Area](../xy-charts/Area) component's cursor property.

Note that while our variables follow this convention, it does not guarantee that the value you wish
to override is available. Be sure to check the corresponding doc page of the component you want to customize
to see the available CSS variables.

### Basic Example
Variables can be overridden in your CSS style declarations. Consider the default configuration for sankey, which looks like this:

<DocWrapper {...sankeyProps()} subLabel={d => Math.random()} excludeTabs/>

Now consider the following style declaration. After adding `custom-sankey` to the container element of the
_Sankey_ component, we will see the following result:

<CSSVariables selector='.custom-sankey'
    vars={[
  '--vis-sankey-node-sublabel-font-weight',
  '--vis-sankey-node-color',
  '--vis-sankey-link-color',
  '--vis-sankey-link-opacity'
]}/>


<div className='custom-sankey'>
   <DocWrapper {...sankeyProps()} subLabel={d => Math.random()} excludeTabs/>
</div>

### Dark Theme Usage
Our library offers dark theme support which takes effect when the class `theme-dark` is added to the
document's `body` element. Every component has a dark version of each color variable labeled with the
prefix `--vis-dark`. You can opt not to override these if you want to use our default dark theme values,
or override them like so:

<CSSVariables selector='.custom-sankey'
    vars={[
  '--vis-sankey-node-color',
  '--vis-dark-sankey-node-color',
  '--vis-sankey-link-color',
  '--vis-dark-sankey-link-color',
]}/>

## Global Variables
The majority of our variables exist on a component level,  but there are a few global CSS variables:

<CSSVariables selector=':root' vars={[
  '--vis-font-family',
  '--vis-color-main',
  '--vis-color-main-light',
  '--vis-color-main-dark',
  '--vis-color-grey',
]}/>

:::note
Unless overridden explicitly, `--vis-color-main` corresponds to the first color in the default [color palette](#color-palette)
:::

## Label Styling
### Font
The font for labels across all of our components is defined by the `--vis-font-family` variable. The default font,
[Inter](https://rsms.me/inter/), is not imported by default, but you can easily import it yourself from
[Google Fonts](https://fonts.google.com/specimen/Inter).

To use a different font, simply redefine the `--vis-font-family` CSS variable:

<div className='custom-font'>
  <CSSVariables vars={['--vis-font-family']} selector='.custom-font'/>
  <XYWrapper
    {...scatterProps}
    labelPosition='center'
    data={generateScatterDataRecords(10)}
    sizeRange={[40,60]}
  />
</div>

### Large Sizing
A common theming scenario is the "large size" theme, for when you want larger font sizes for the
labels in your charts. We offer two variations in the form of css classes that you can import directly
from `@unovis/ts`:

```ts
import { styleLargeSize } from '@unovis/ts' // ~1.3x larger
import { styleExtraLargeSize } from '@unovis/ts' // 2x larger
```

Just add either one to your container's class list to the effects. Consider the following example
of a labeled _Scatter_ chart:

<SizedScatter/>

#### `className: styleLargeSize`
<SizedScatter containerClass={'styleLargeSize'}/>

#### `className: styleExtraLargeSize`
<SizedScatter containerClass={'styleExtraLargeSize'}/>

:::note
When using this theme, the following components have caveats:

- [**Scatter**](../xy-charts/Scatter):
If the [`labelPosition`](../xy-charts/Scatter#label-position) property is set to `Position.Center`,
point labels will try fit to the point's size. In this case, you will instead need to update the
[`pointSize`](../xy-charts/Scatter#size-and-size-range) property to render larger labels.

- [**Timeline**](../xy-charts/Timeline)
Additionally, you may need to adjust the [`rowHeight`](../xy-charts/Timeline#row-height) property to
accommodate larger labels.
:::

## Color Palette
Many of our components use the default color palette for visualizations. You can import the array of hex values directly
from `unovis/ts`:
```ts
import { colors, colorsDark } from '@unovis/ts'
```

The dark theme palette is slightly different from the regular one. These colors are also defined directly in our CSS variables, labeled `--vis-color0`, `--vis-dark-color0`, `--vis-color1`, `--vis-dark-color1`, etc.
The full palette looks like this:
#### Light
<CSSVariables className='color-vars' selector=':root' vars={Array(6).fill(0).map((_, i) => `--vis-color${i}`)} />

#### Dark
<CSSVariables className='color-vars-dark' selector=':root' vars={Array(6).fill(0).map((_, i) => `--vis-dark-color${i}`)} />

#### Palette Editor
You can tweak and preview your desired palette using the example _StackedBar_ component below.
If you like the result, just copy and paste the corresponding style declaration in the dropdown below.

<BrowserOnly>
{() => {
  const ref = React.useRef(null)
  const { colors } = require('@unovis/ts')
  const colorMap = Object.fromEntries(colors.map((c, i) => [`--vis-color${i}`, c]))
  const [variables, setVariables] = React.useState(colorMap)
  function update(event, key) {
    setVariables({...variables, [key]: event.target.value})
    ref.current.style.setProperty(key, event.target.value)
  }
  return (
    <div id='custom-vis' ref={ref}>
      <div className='side-by-side'>
        <table>
          <thead>
            <tr>
              <th>Variable</th>
              <th>Color</th>
              <th>Hex</th>
            </tr>
          </thead>
          <tbody>
          {Object.keys(variables).map(c => (
            <tr key={c}>
              <td>{c}</td>
              <td><input type='color' value={variables[c]} onChange={e => update(e, c)}/></td>
              <td><input type='text' value={variables[c]} onChange={e => update(e, c)}/></td>
            </tr>
          ))}
        </tbody>
      </table>
      <XYWrapper name='StackedBar' className='custom-vis' {...xyProps(colors.length)}/>
    </div>
    <details>
      <summary>styles.css</summary>
        <CodeBlock language='css'>
        {`:root {\n  ${Object.entries(variables).map(([k, v]) => [k, v].join(': ')).join(';\n  ')};\n}`}
      </CodeBlock>
    </details>
  </div>
  )
}}
</BrowserOnly>

:::tip
Alternatively, you can provide a custom color palette in *global scope* using the `UNOVIS_COLORS` variable:
```javascript
window.UNOVIS_COLORS = [...]
// or
globalThis.UNOVIS_COLORS = [...]
```
This needs to be done before the library is imported, i.e. in your top level JS file or HTML.
:::

## Applying Patterns
When `document.body` has the class `theme-patterns` we automatically apply patterns of two types:

### Fill Patterns
For solid shapes (most cases). You can customize fill patterns by assigning the corresponding CSS
to a SVG [`mask`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/mask) reference.

The fill pattern palette looks like:

<body class='theme-patterns' style={{ marginBottom: '20px'}}>
  <XYWrapper
    name='StackedBar'
    data={Array(6).fill(0).map((_, i) => ({ x: i, y: 1 + Math.random(), color: `var(--vis-color${i})` }))}
    x={d => d.x}
    y={d => d.y}
    color={d => d.color}
    height={100}
    components={[axis('x', { numTicks: 6, tickFormat: i => `Fill Pattern #${i}`})]}
    excludeTabs
  />
</body>

<details open>
<summary>Default CSS Variables:</summary>

```css
--vis-pattern-fill0: var(--vis-pattern-fill-stripes-diagonal);
--vis-pattern-fill1: var(--vis-pattern-fill-dots);
--vis-pattern-fill2: var(--vis-pattern-fill-stripes-vertical);
--vis-pattern-fill3: var(--vis-pattern-fill-crosshatch);
--vis-pattern-fill4: var(--vis-pattern-fill-waves);
--vis-pattern-fill5: var(--vis-pattern-fill-circles);
```

</details>

### Line Patterns
For the _Line_ component and when _BulletLegend_'s `bulletShape` property is set to `"line"`.
You can customize these patterns by assigning any combination of the following variable types:
- Prefixed `--vis-pattern-marker`: accepts SVG defs containings
[`marker`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/marker) elements
- Variables with the prefix `--vis-pattern-dasharray` to a valid value for the
[stroke-dasharray](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray) property. The default palette looks like:
  
<body class='theme-patterns' style={{ marginBottom: '20px'}}>
  <XYWrapper
    name='Line'
    {...xyProps(6)}
    y={Array(6).fill(0).map((_, i) => (d, j) => i + (j === 0 ? 0.5 : d.values[i]))}
    components={[axis('y', {
      tickValues: Array(6).fill(0).map((_, i) => i + 0.5),
      tickFormat: i => `Line Pattern #${Math.floor(i)}`
    })]}
    excludeTabs
  />
</body>

  
<details>
<summary>Default CSS Variables:</summary>

```css
--vis-pattern-marker0: var(--vis-pattern-marker-circle);

--vis-pattern-marker1: var(--vis-pattern-marker-triangle);
--vis-pattern-dasharray1: 9 1;

--vis-pattern-marker2: var(--vis-pattern-marker-diamond);
--vis-pattern-dasharray2: 2;

--vis-pattern-marker3: var(--vis-pattern-marker-arrow);
--vis-pattern-dasharray3: 2 3 8 3;

--vis-pattern-marker4: var(--vis-pattern-marker-square);
--vis-pattern-dasharray4: 6;

--vis-pattern-marker5: var(--vis-pattern-marker-star);
--vis-pattern-dasharray5: 1 6;
```

</details>

### Summary
To override default patterns use the following table for reference.

| CSS Variable Prefix       | Type | Accepted Value | Example |
| ------------------------- | -----| ----------- | --------------- |
| `--vis-pattern-fill`      | Fill | SVG [`mask`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/mask) from a `<defs>` element | `url(#my-pattern-fill)` |
| `--vis-pattern-marker`    | Line | SVG [`marker`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/marker) from a `<defs>` element | `url(#my-line-marker)` |
| `--vis-pattern-dasharray` | Line | CSS [stroke-dasharray](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray) property | `5 10` |


## Bordered Segments
For charts with multiple data layers, it might be preferable to have a visual separation of elements.
You can do this by manipulating the `stroke` and `stroke-width` variables to create a bordered segment
effect.

For the following components, the `stroke` property by default is either `none` or the same color
as its fill. You can tweak the variables accordingly to create the desired effect:

```css
:root  {
  --stroke: #fff;
  --stroke-dark: #292b34;

  /* Area */
  --vis-area-stroke-width: 1px;
  --vis-area-stroke: var(--stroke);
  --vis-dark-area-stroke: var(--stroke-dark);

  /* Donut */
  --vis-donut-segment-stroke-width: 1px;

  /* StackedBar */
  --vis-stacked-bar-stroke-width: 1px;
  --vis-stacked-bar-stroke: var(--stroke);
  --vis-dark-stacked-bar-stroke: var(--stroke-dark);

  /* Timeline */
  --vis-timeline-line-stroke-width: 1px;
}
```

<div className='border-lines'>
  <h4>Area</h4>
  <XYWrapper name='Area' {...borderLineProps} width={500}/>
  <h4>Donut</h4>
  <DocWrapper {...donutProps()} {...borderLineProps} value={d => Math.random()} arcWidth={50}/>
  <h4>Stacked Bar</h4>
  <XYWrapper name='StackedBar' {...borderLineProps}/>
  <h4>Timeline</h4>
  <XYWrapper name='Timeline' data={generateTimeSeries(50, 6)} height={200} x={d => d.timestamp} length={d => d.length * 10} type={d => d.type} lineCap excludeTabs/>
</div>
